package main

import (
	"excel2lua/gen"
	"excel2lua/meta"
	"fmt"
	"os"
	"sort"
	"strings"
	"unicode"

	"github.com/spf13/viper"
	"github.com/bytedance/sonic"
	"github.com/samber/lo"
	"github.com/xuri/excelize/v2"
)

type Config struct {
	Input string `json:"input" yaml:"input"`
	Output string `json:"output" yaml:"output"`
}

func main() {
	viper.SetConfigName("config")
    viper.SetConfigType("yaml")
    viper.AddConfigPath(".")
	//
    if err := viper.ReadInConfig(); err != nil {
		panic(fmt.Errorf("读取配置文件失败: config.yaml, %v", err))
	}
	viper.SetDefault("read-in", ".")
	viper.SetDefault("output", "lua")
	//
    var config Config
    if err := viper.Unmarshal(&config); err != nil {
		panic(fmt.Errorf("unmarshal配置文件失败: config.yaml, %v", err))
	}
	//
	entries, err := os.ReadDir(config.Input)
	if err != nil {
		panic(fmt.Errorf("system falta error, %v", err))
	}
	for _, entry := range entries {
		fileName := entry.Name()
		if !entry.IsDir() && fileName[0] != '~' && strings.HasSuffix(fileName, ".xlsx") && !strings.Contains(fileName, "test") {
			processFile(fileName)
		}
	}
	gen.NewLuaGenerator(config.Output, dumpDataMap(), IgnoreSheet).Gen()
	//--------------------------------------------------------------------------------------------
	fmt.Println("所有xlsx文件已处理，请按【回车键】退出。")
	_, _ = fmt.Scanln()
}

func processFile(fileName string) {
	fmt.Printf("[%s] 开始处理\n", fileName)
	//
	f, err := excelize.OpenFile(fileName)
	if err != nil {
		fmt.Println(err)
		return
	}
	defer func() {
		// Close the spreadsheet.
		if err := f.Close(); err != nil {
			fmt.Println(err)
		}
	}()
	//
	sheets := f.GetSheetList()
	fmt.Printf("[%s] Sheet list: %v\n", fileName, sheets)
	for _, sheet := range sheets {
		if rows, err := f.GetRows(sheet); err != nil {
			fmt.Println(err)
		} else {
			ClassName := sheet
			if strings.HasPrefix(ClassName, ".") { // 处理需要忽略的配置表
				ClassName = strings.TrimPrefix(ClassName, ".")
				IgnoreSheet = append(IgnoreSheet, ClassName)
			}
			DataMap[ClassName] = processSheetData(sheet, rows)
		}
	}
}

func processSheetData(sheetName string, data [][]string) meta.DataStore {
	preTab := "\t"
	rowTab, hasHeader := preTab+"\t", false
	fmt.Printf(preTab+"[%s]-------------------------------------------------------------\n", sheetName)
	var headers []meta.Header
	var objects []meta.StoreItem // map[string]map[string]string = make(map[string]map[string]string)
	var err error
	for rowIdx, values := range data {
		if rowIdx == 0 { // header row
			fmt.Printf(rowTab+"[%d]: %+v\n", rowIdx, values) // log
			if headers, err = parseHeader(values); err != nil || len(headers) == 0 {
				break
			} else {
				hasHeader = true
			}
		} else { // data row
			obj, propSize, Id := make(map[string]string), len(values), ""
			for _, prop := range headers {
				var propValue string
				if prop.Col >= propSize { // out of range
					propValue = getDefaultValue(prop)
				} else if propValue, err = parseValue(prop, values[prop.Col]); err != nil {
					fmt.Printf(rowTab+"[%d]: %+v\n", rowIdx, values) // log
				} else if prop.Name == KeyProp { // id prop
					if len(strings.TrimSpace(propValue)) == 0 {
						fmt.Printf(rowTab+"[%d]: KeyProp[%s] is not define\n", rowIdx, KeyProp) // log
					} else {
						Id = strings.TrimFunc(propValue, func(r rune) bool { return r == '\'' || unicode.IsSpace(r) })
					}
				}
				obj[prop.Name] = propValue
			}
			//
			objects = append(objects, meta.StoreItem{Key: Id, Object: obj})
			// objects[Id] = obj
		}
	}
	if !hasHeader {
		if err != nil {
			fmt.Printf(preTab+"[%s] Parse Header Failed: Skip, %v, %v\n", sheetName, headers, err)
		} else {
			fmt.Printf(preTab+"[%s] No Header Row: Skip, %v\n", sheetName, headers)
		}
	}
	sort.Slice(objects, func(i, j int) bool {
		return objects[i].Key < objects[j].Key
	})
	return meta.DataStore{Headers: headers, List: objects}
}

func parseHeader(hs []string) ([]meta.Header, error) {
	var ret []meta.Header
	for colIdx, hd := range hs {
		if !strings.HasPrefix(hd, "{") {
			// ignore
		} else {
			hd = strings.TrimFunc(hd, func(r rune) bool {
				return r == ' ' || r == '{' || r == '}'
			})
			nat := strings.Split(hd, ",")
			if len(nat) >= 2 {
				if isSupportDataType(nat[1]) {
					rc := ""
					if len(nat) > 2 {
						rc = nat[2]
					}
					ret = append(ret, meta.Header{Name: nat[0], VType: nat[1], Col: colIdx, RefClass: rc})
				} else {
					return ret, fmt.Errorf("invalid Header Define[%s]. data type[%s] is not supported. %v", hd, nat[1], SupportDataType)
				}
			} else {
				return ret, fmt.Errorf("invalid Header Define[%s]", hd)
			}
		}
	}
	if len(ret) >= 0 {
		if _, ok := lo.Find(ret, func(it meta.Header) bool {
			return it.Name == KeyProp
		}); !ok {
			return ret, fmt.Errorf("invalid Header Define: Not Found KeyProp[:%s]", KeyProp)
		}
	}
	return ret, nil
}

func orDefault(value, def string) string {
	if len(value) > 0 {
		return value
	}
	return def
}

func parseValue(hDefine meta.Header, value string) (string, error) {
	vt := strings.ToLower(hDefine.VType)
	if vt == "int" || vt == "num" || vt == "number" {
		return orDefault(value, "0"), nil
	} else if vt == "bool" {
		return orDefault(strings.ToLower(value),"false"), nil
	} else if vt == "string" || vt == "str" {
		return fmt.Sprintf("'%s'", value), nil
	} else if vt == "json" || vt == "table" {
		return orDefault(value, "{}"), nil
	} else if strings.Contains(vt, "ref") {
		return fmt.Sprintf("'%s:%s:%s'", vt, hDefine.RefClass, value), nil
	} else {
		return value, fmt.Errorf("unsupport data type: header = %s, type = %s. data type is one of [int,num,number,string,str,json,table]", hDefine.Name, hDefine.VType)
	}
}

func getDefaultValue(hDefine meta.Header) string {
	vt := strings.ToLower(hDefine.VType)
	if vt == "int" || vt == "num" || vt == "number" {
		return "0"
	} else if vt == "string" || vt == "str" {
		return "''"
	} else if vt == "json" || vt == "table" || vt == "ref_list" || vt == "ref" || vt == "ref_map" {
		return "{}"
	} else if vt == "bool" {
		return "false"
	} else {
		return "''"
	}
}

func isSupportDataType(vt string) bool {
	return lo.IndexOf(SupportDataType, vt) != -1
}

func dumpDataMap() map[string]meta.DataStore {
	var m map[string]meta.DataStore
	output, _ := sonic.Marshal(DataMap)
	sonic.Unmarshal(output, &m)
	return m
}

var DataMap map[string]meta.DataStore = make(map[string]meta.DataStore)
var SupportDataType = []string{"int", "num", "number", "string", "str", "json", "table", "ref", "ref_list", "ref_map", "bool"}
var IgnoreSheet []string

const KeyProp = "Id"
